/*
  Copyright (c) 2012 The Chromium Authors. All rights reserved.
  Use of this source code is governed by a BSD-style license that can be
  found in the LICENSE file.
*/

/*
  Fetch all graph data files, prepare the data, and create Plotter() to
  generate a graph.

  To use:
  var graph = new Graph('output_div', graphList)
  graph.setTitle('Title');
  graph.graph();
*/

function JsonToJs(data) {
  return eval('(' + data + ')');
}

/**
 * Insert element a after element b.
 */
function AppendChild(a, b) {
  var elementA = (typeof(a) == 'object') ? a : document.getElementById(a);
  var elementB = (typeof(b) == 'object') ? b : document.getElementById(b);
  elementB.appendChild(elementA);
}

/**
 * Insert element a before element b.
 */
function InsertBefore(a, b) {
  var elementA = (typeof(a) == 'object') ? a : document.getElementById(a);
  var elementB = (typeof(b) == 'object') ? b : document.getElementById(b);
  elementB.insertBefore(elementA);
}


/**
 * Graph class.
 * @constructor
 *
 * Fetch each graph in |graphList| and create Plotter().  Create Graph()
 * and call graph() to display graph.
 *
 * @param div {String|DOMElement} The container that the graph should be
 *            rendered to.
 * @param graphList {Array} List of graphs to be plotted.
 * @param options {Object} Options to configure graph.
 *          - width {int} Width of graph.
 *          - height {int} Height of graph.
 *          - history {int} Number of row to show.
 *          - showDetail {Boolean} Specifies whether or not to display detail.
 *            Default false.
 *          - showTabs {Boolean} Specifies whether or not to show tabs.
 *            Default false.
 *          - enableMouseScroll {Boolean} Specifies whether or not to enable
 *            mouse wheel zooming. Default false.
 *          - channels {Array} Display graph by channels.
 *          - orderDataByVersion {Boolean} Order plot data by version number.
 *            Default false.
 *
 * Example of the graphList:
 *  [
 *    {"units": "ms", "important": false, "name": "SunSpider-individual",
 *     "SunSpider-individual-summary.dat"},
 *  ]
 */
function Graph(div, graphList, opt_options) {
  this.graphList_ = graphList;
  this.options_ = (opt_options) ? opt_options : {};
	this.history_ = (this.options_.history) ? this.options_.history : 150;
	this.rev_ = (this.options_.rev) ? this.options_.rev : -1;
	this.channels_ = (this.options_.channels) ? this.options_.channels : [];
	this.firstTrace_ = '';
  this.rows_ = [];
  this.isDetailViewAdded_ = false;
  this.selectedGraph_ = null;
  this.plotterDiv_ = null;
  this.tabs_ = [];
  this.width = this.options_.width;
  this.height = this.options_.height;

  this.graphContainer = document.createElement('div');
  this.graphContainer.setAttribute(
      'style', 'display: block; overflow: hidden; ' +
      'margin: 5px; padding: 5px; width:' + this.width);
  AppendChild(this.graphContainer, div);

  this.title = document.createElement('div');
  this.title.setAttribute('style', 'text-align: center');
  AppendChild(this.title, this.graphContainer);
}

/**
 * Start fetching graph data.
 */
Graph.prototype.graph = function() {
  this.fetchSummary_();

  if (this.options_.showTabs)
    this.addTabs_();
}

/**
 * Set graph title.
 */
Graph.prototype.setTitle = function(title) {
    this.title.innerHTML = title;
}

/**
 * Display tabs for each graph.
 */
Graph.prototype.addTabs_ = function() {
  this.tabs_ = [];
  var tabPane = document.createElement('div');
  tabPane.setAttribute('class', 'switcher');
  AppendChild(tabPane, this.graphContainer);

  var graphNames = []
  var inserted = {};
	for (var i = 0; i < this.graphList_.length; i++) {
    if (!inserted[this.graphList_[i].name]) {
      graphNames.push(this.graphList_[i].name);
      inserted[this.graphList_[i].name] = 1;
    }
  }

  var obj = this;
  for (var i = 0; i < graphNames.length; i++) {
    var name = graphNames[i];
		var tab = document.createElement('span');
    if (name != this.selectedGraph_.name) {
      tab.setAttribute('class', 'select');
    }
    tab.addEventListener(
        "click",
        (function(){
          var cur = name; return function() {obj.switchGraph_(cur)}
        })(),
        false);
    tab.appendChild(document.createTextNode(name + " "));
		AppendChild(tab, tabPane);
    this.tabs_.push(tab);
  }
}

/**
 * Fetch graph summary data files.
 */
Graph.prototype.fetchSummary_ = function() {
  this.rows_ = [];
  if (!this.selectedGraph_) {
    this.selectedGraph_ = this.graphList_[0];
  }
  var graphFiles = [];
  this.selectedGraphList_ = [];
  for (var i = 0; i < this.graphList_.length; i++) {
    if (this.graphList_[i].name == this.selectedGraph_.name) {
      graphFiles.push(this.graphList_[i].loc);
      this.selectedGraphList_.push(this.graphList_[i]);
    }
  }
  var obj = this;
  new FetchList(graphFiles, function(data) {obj.onSummaryReceived_(data)});
}

/**
 * Call addPlot_ once all graph summary data are received.
 */
Graph.prototype.onSummaryReceived_ = function(data) {
  // Parse the summary data file.
  for (var i = 0; i < data.length; i++) {
    if (data[i]) {
      var rows = new Rows(data[i]);
      this.rows_[i] = rows;
    }
  }
  this.addPlot_();
}

/**
 * Merge all data rows by channel and version.  This is use in platform
 * comparison graph.
 * 
 * Example:
 *   Two rows:
 *     {"traces": {"score": ["777", "0.0"]}, "rev": "9",
 *      "ver": "17.1.963.19", "chan": "stable"}
 *     {"traces": {"score": ["888", "0.0"]}, "rev": "10",
 *      "ver": "17.1.963.19", "chan": "stable"}
 *   Become:
 *     {"traces": {"score_windows": ["777", "0.0"], 
 *                 "score_linux": ["888", "0.0"]},
 *      "rev": "9", "ver": "17.1.963.19", "chan": "stable"}
 *
 * @return {Array} Array of rows.
 */
Graph.prototype.getMergedRowsByVersion_ = function() {
  var channels = {};
  for (var i = 0; i < this.channels_.length; i++)
     channels[this.channels_[i]] = 1;
  var allRows = [];
  // Combind all rows to one list.
  for (var i = 0; i < this.rows_.length; i++) {
    if (this.rows_[i]) {
      for (var j = 0; j < this.rows_[i].length; j++) {
        var row = this.rows_[i].get(j);
        if (row && row.chan in channels) {
          row.machine = this.selectedGraphList_[i].machine;
          allRows.push(row);
        }
      }
    }
  }

  // Sort by version number.
  allRows.sort(
      function(a, b) {
        var a_arr = a.version.split('.');
        var b_arr = b.version.split('.');
        var len = Math.min(b_arr.length, b_arr.length);
        for (var i = 0; i < len; i++) {
          if (parseInt(a_arr[i], 10) > parseInt(b_arr[i], 10))
            return 1;
          else if (parseInt(a_arr[i], 10) < parseInt(b_arr[i], 10))
            return -1;
        }
        return a_arr.length - b_arr.length;
      });

  // Merge all rows by version number.
  var combindedRows = [];
  var index = 0;
  while (index < allRows.length) {
    var currentRow = allRows[index];
    var traces = currentRow['traces'];
    for (var traceName in traces) {
      var traceRenamed = traceName + '_' + currentRow.machine.toLowerCase();
      traces[traceRenamed] = traces[traceName];
      delete(traces[traceName]);
    }
    while (index < allRows.length - 1 &&
           allRows[index + 1].version == currentRow.version) {
      var row = allRows[index + 1];
      var traces = row['traces'];
      for (var traceName in traces) {
        var traceRenamed = traceName + '_' + row.machine.toLowerCase();
        currentRow['traces'][traceRenamed] = traces[traceName];
      }
      index++;
    }
    combindedRows.push(currentRow);
    index++;
  }
  return combindedRows;
}

/**
 * Merge all channel data by their index in file.  This is use in channel
 * comparison graph.
 *
 * @return {Array} Array of rows.
 */
Graph.prototype.getMergedRowByIndex_ = function() {
  var rowByChannel = {};
  for (var i = 0; i < this.channels_.length; i++)
    rowByChannel[this.channels_[i]] = [];

  // Order by channel.
  for (var i = 0; i < this.rows_.length; i++) {
    if (this.rows_[i]) {
      for (var j = 0; j < this.rows_[i].length; j++) {
        var row = this.rows_[i].get(j);
        if (row && row.chan in rowByChannel) {
          rowByChannel[row.chan].push(row);
        }
      }
    }
  }

  var max = 0;
  for (var channel in rowByChannel)
    max = Math.max(rowByChannel[channel].length, max);

  // Merge data.
  var combindedRows = [];
  for (var i = 0; i < max; i++) {
    var currentRow = null;
    for (var channel in rowByChannel) {
      if (rowByChannel[channel].length > i) {
        var row = rowByChannel[channel][i];
        var traces = row['traces'];
        for (var traceName in traces) {
          traces[traceName + '_' + channel] = traces[traceName];
          delete(traces[traceName]);
        }
        if (!currentRow) {
          currentRow = row;
        } else {
          for (var traceName in traces)
            currentRow['traces'][traceName] = traces[traceName];
          currentRow.version += ', ' + row.version;
        }
      }
    }
    combindedRows.push(currentRow);
  }
  return combindedRows;
}

/**
 * Get rows for a specific channel.
 *
 * @return {Array} Array of rows.
 */
Graph.prototype.getRowByChannel_ = function() {
  // Combind channel data.
  var rows = [];
  for (var i = 0; i < this.rows_.length; i++) {
    if (this.rows_[i]) {
      for (var j = 0; j < this.rows_[i].length; j++) {
        var row = this.rows_[i].get(j);
        if (row && row.chan == this.channels_[0])
          rows.push(row);
      }
    }
  }
  return rows;
}

/**
 * Prepare the data and create Plotter() to generate a graph.
 */
Graph.prototype.addPlot_ = function() {
  var rows = [];
  if (this.options_.orderDataByVersion)
    rows = this.getMergedRowsByVersion_();
  else if (this.channels_.length > 1)
    rows = this.getMergedRowByIndex_();
  else
    rows = this.getRowByChannel_();

  var maxRows = rows.length;
  if (maxRows > this.history_)
    maxRows = this.history_;

  // Find the start and end of the data slice we will focus on.
  var startRow = 0;
  if (this.rev_ > 0) {
    var i = 0;
    while (i < rows.length) {
      var row = rows[i];
      // If the current row's revision is higher than the desired revision,
      // continue searching.
      if (row.revision > this.rev_) {
        i++;
        continue;
      }
      // We're either just under or at the desired revision.
      startRow = i;
      // If the desired revision does not exist, use the row before it.
      if (row.revision < this.rev_ && startRow > 0)
        startRow -= 1;
      break;
    }
  }

  // Some summary files contain data not listed in rev-descending order.  For
  // those cases, it is possible we will find a start row in the middle of the
  // data whose neighboring data is not nearby.  See xp-release-dual-core
  // moz rev 265 => no graph.
  var endRow = startRow + maxRows;

  // Build and order a list of revision numbers.
  var allTraces = {};
  var revisionNumbers = [];
  var versionMap = {};
  var hasNumericRevisions = true;
  // graphData[rev] = {trace1:[value, stddev], trace2:[value, stddev], ...}
  var graphData = {};
  for (var i = startRow; i < endRow; ++i) {
    var row = rows[i];
    if (!row)
      continue;
    var traces = row['traces'];
    for (var j = 0; j < traces.length; ++j)
      traces[j] = parseFloat(traces[j]);

    graphData[row.revision] = traces;
    if (isNaN(row.revision)) {
      hasNumericRevisions = false;
    }
    revisionNumbers.push(row.revision);

    versionMap[row.revision] = row.version;

    // Collect unique trace names.  If traces are explicitly specified in
    // params, delete unspecified trace data.
    for (var traceName in traces) {
      if (typeof(params['trace']) != 'undefined' &&
          params['trace'][traceName] != 1) {
        delete(traces[traceName]);
      }
      allTraces[traceName] = 1;
    }
  }

  // Build a list of all the trace names we've seen, in the order in which
  // they appear in the data file. Although JS objects are not required by
  // the spec to iterate their properties in order, in practice they do,
  // because it causes compatibility problems otherwise.
  var traceNames = [];
  for (var traceName in allTraces)
    traceNames.push(traceName);
  this.firstTrace_ = traceNames[0];

  // If the revisions are numeric (svn), sort them numerically to ensure they
  // are in ascending order. Otherwise, if the revisions aren't numeric (git),
  // reverse them under the assumption the rows were prepended to the file.
  if (hasNumericRevisions) {
    revisionNumbers.sort(
        function(a, b) { return parseInt(a, 10) - parseInt(b, 10) });
  } else {
    revisionNumbers.reverse();
  }

  // Build separate ordered lists of trace data.
  var traceData = {};
  var versionList = [];
  for (var revIndex = 0; revIndex < revisionNumbers.length; ++revIndex) {
    var rev = revisionNumbers[revIndex];
    var revisionData = graphData[rev];
    for (var nameIndex = 0; nameIndex < traceNames.length; ++nameIndex) {
      var traceName = traceNames[nameIndex];
      if (!traceData[traceName])
        traceData[traceName] = [];
      if (!revisionData[traceName])
        traceData[traceName].push([NaN, NaN]);
      else
        traceData[traceName].push([parseFloat(revisionData[traceName][0]),
                                   parseFloat(revisionData[traceName][1])]);
    }
    versionList.push(versionMap[rev]);
  }

  var plotData = [];
  for (var traceName in traceData)
    plotData.push(traceData[traceName]);

  var plotterDiv = document.createElement('div');
  if (!this.plotterDiv_)
    AppendChild(plotterDiv, this.graphContainer)
  else
    this.graphContainer.replaceChild(plotterDiv, this.plotterDiv_);
  this.plotterDiv_ = plotterDiv;

  var plotter = new Plotter(
      versionList, plotData, traceNames, this.selectedGraph_.units,
      this.plotterDiv_, this.width, this.height);

  var obj = this;
  plotter.onclick = function(){obj.onPlotClicked.apply(obj, arguments)};
  plotter.enableMouseScroll = this.options_.enableMouseScroll;
  plotter.plot();
}

/**
 * Handle switching graph when tab is clicked.
 */
Graph.prototype.switchGraph_ = function(graphName) {
  if (graphName == this.selectedGraph_.name)
     return;

  for (var i = 0; i < this.tabs_.length; i++) {
    var name = this.tabs_[i].innerHTML;
    if (graphName + ' ' == name) {
      this.tabs_[i].removeAttribute('class');
    } else {
      this.tabs_[i].setAttribute('class', 'select');
    }
  }

  for (var i = 0; i < this.graphList_.length; i++) {
    if (this.graphList_[i].name == graphName) {
      this.selectedGraph_ = this.graphList_[i];
      break;
    }
  }

  this.fetchSummary_();
}

/**
 * On plot clicked, display detail view.
 */
Graph.prototype.onPlotClicked = function(prev_cl, cl) {
  if (!this.options_.showDetail)
    return;
  this.addDetailView_();

  var getChildByName = function(div, name) {
    var children = div.childNodes;
    for (var i = 0; i < children.length; i++)
      if (children[i].getAttribute('name') == name)
        return children[i];
  }
  // Define sources for detail tabs
  if ('view-change' in Config.detailTabs) {
    // If the changeLinkPrefix has {PREV_CL}/{CL} markers, replace them.
    // Otherwise, append to the URL.
    var url = Config.changeLinkPrefix;
    if (url.indexOf('{PREV_CL}') >= 0 || url.indexOf('{CL}') >= 0) {
      url = url.replace('{PREV_CL}', prev_cl);
      url = url.replace('{CL}', cl);
    } else {
      url += prev_cl + ':' + cl;
    }
    getChildByName(this.detailPane, 'view-change').setAttribute('src', url);
  }

  if ('view-pages' in Config.detailTabs) {
    getChildByName(this.detailPane, 'view-pages').
        setAttribute('src', 'details.html?cl=' + cl +
            '&graph=' + this.milestone + '-' + this.selectedGraph_.name +
            '&trace=' + this.firstTrace_);
  }
  if ('view-coverage' in Config.detailTabs) {
    getChildByName(this.detailPane, 'view-coverage').setAttribute(
        'src',Config.coverageLinkPrefix + cl);
  }

  if (!this.didPositionDetail) {
    this.positionDetails_();
    this.didPositionDetail = true;
  }
}

/**
 * Display detail view.
 */
Graph.prototype.addDetailView_ = function() {
  if (this.isDetailViewAdded_)
    return;
  this.isDetailViewAdded_ = true;
  // Add detail page.
  this.detailPane = document.createElement('div');
  AppendChild(this.detailPane, this.graphContainer);

  for (var tab in Config.detailTabs) {
    var detail = document.createElement('iframe');
    detail.setAttribute('class', 'detail');
    detail.setAttribute('name', tab);
    AppendChild(detail, this.detailPane);
  }

  this.selectorPane = document.createElement('div');
  this.selectorPane.setAttribute('class', 'selectors');
  this.selectorPane.setAttribute(
      'style', 'display: block; 1px solid black; position: absolute;');
  AppendChild(this.selectorPane, this.graphContainer);

  var firstTab = true;
  for (var tab in Config.detailTabs) {
    var selector = document.createElement('div');
    selector.setAttribute('class', 'selector');
    var obj = this;
    selector.onclick = (
        function(){
            var cur = tab; return function() {obj.changeDetailTab(cur)}})();
    if (firstTab)
      firstTab = false;
    else
      selector.setAttribute('style', 'border-top: none');
    selector.innerHTML = Config.detailTabs[tab];
    AppendChild(selector, this.selectorPane);
  }
}

Graph.prototype.positionDetails_ = function() {
  var win_height = window.innerHeight;

  var views_width = this.graphContainer.offsetWidth -
                    this.selectorPane.offsetWidth;

  this.detailPane.style.width = views_width + "px";
  this.detailPane.style.height = (
      win_height - this.graphContainer.offsetHeight -
      this.graphContainer.offsetTop - 30) + "px";

  this.selectorPane.style.left = (
      this.detailPane.offsetLeft + views_width + 1) + "px";
  this.selectorPane.style.top = this.detailPane.offsetTop + "px";

  // Change to the first detail tab
  for (var tab in Config.detailTabs) {
    this.changeDetailTab_(tab);
    break; 
  }
}

Graph.prototype.changeDetailTab_ = function(target) {
  var detailArr = this.detailPane.getElementsByTagName('iframe');
  var i = 0;
  for (var tab in Config.detailTabs) {
    detailArr[i++].style.display = (tab == target ? 'block' : 'none');
  }
}

Graph.prototype.goTo = function(graph) {
  params.graph = graph;
  if (params.graph == '')
    delete params.graph;
  window.location.href = MakeURL(params);
}

Graph.prototype.getURL = function() {
  new_url = window.location.href;
  new_url = new_url.replace(/50/, "150");
  new_url = new_url.replace(/\&lookout=1/, "");
  return new_url;
}


/**
 * Encapsulates a *-summary.dat file.
 * @constructor
 */
function Rows(data) {
  this.rows_ = (data) ? data.split('\n') : [];
  this.length = this.rows_.length;
}

/**
 * Returns the row at the given index.
 */
Rows.prototype.get = function(i) {
  if (!this.rows_[i].length) return null;
  var row = JsonToJs(this.rows_[i]);
  row.revision = isNaN(row['rev']) ? row['rev'] : parseInt(row['rev']);
  row.version = row['ver'];
  return row;
};
